/*
 * chap-new.c - New CHAP implementation.
 *
 * Copyright (c) 2003 Paul Mackerras. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. The name(s) of the authors of this software must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission.
 *
 * 3. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by Paul Mackerras
 *     <paulus@samba.org>".
 *
 * THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO
 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
 * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "netif/ppp/ppp_opts.h"
#if PPP_SUPPORT &&                                                             \
    CHAP_SUPPORT /* don't build if not configured for use in lwipopts.h */

#if 0 /* UNUSED */
#include <stdlib.h>
#include <string.h>
#endif /* UNUSED */

#include "netif/ppp/ppp_impl.h"

#if 0 /* UNUSED */
#include "session.h"
#endif /* UNUSED */

#include "netif/ppp/chap-md5.h"
#include "netif/ppp/chap-new.h"
#if MSCHAP_SUPPORT
#include "netif/ppp/chap_ms.h"
#endif
#include "netif/ppp/magic.h"

#if 0  /* UNUSED */
/* Hook for a plugin to validate CHAP challenge */
int (*chap_verify_hook)(const char *name, const char *ourname, int id,
			const struct chap_digest_type *digest,
			const unsigned char *challenge, const unsigned char *response,
			char *message, int message_space) = NULL;
#endif /* UNUSED */

#if PPP_OPTIONS
/*
 * Command-line options.
 */
static option_t chap_option_list[] = {
    {"chap-restart", o_int, &chap_timeout_time, "Set timeout for CHAP",
     OPT_PRIO},
    {"chap-max-challenge", o_int, &pcb->settings.chap_max_transmits,
     "Set max #xmits for challenge", OPT_PRIO},
    {"chap-interval", o_int, &pcb->settings.chap_rechallenge_time,
     "Set interval for rechallenge", OPT_PRIO},
    {NULL}};
#endif /* PPP_OPTIONS */

/* Values for flags in chap_client_state and chap_server_state */
#define LOWERUP 1
#define AUTH_STARTED 2
#define AUTH_DONE 4
#define AUTH_FAILED 8
#define TIMEOUT_PENDING 0x10
#define CHALLENGE_VALID 0x20

/*
 * Prototypes.
 */
static void chap_init(ppp_pcb *pcb);
static void chap_lowerup(ppp_pcb *pcb);
static void chap_lowerdown(ppp_pcb *pcb);
#if PPP_SERVER
static void chap_timeout(void *arg);
static void chap_generate_challenge(ppp_pcb *pcb);
static void chap_handle_response(ppp_pcb *pcb, int code, unsigned char *pkt,
                                 int len);
static int chap_verify_response(ppp_pcb *pcb, const char *name,
                                const char *ourname, int id,
                                const struct chap_digest_type *digest,
                                const unsigned char *challenge,
                                const unsigned char *response, char *message,
                                int message_space);
#endif /* PPP_SERVER */
static void chap_respond(ppp_pcb *pcb, int id, unsigned char *pkt, int len);
static void chap_handle_status(ppp_pcb *pcb, int code, int id,
                               unsigned char *pkt, int len);
static void chap_protrej(ppp_pcb *pcb);
static void chap_input(ppp_pcb *pcb, unsigned char *pkt, int pktlen);
#if PRINTPKT_SUPPORT
static int chap_print_pkt(const unsigned char *p, int plen,
                          void (*printer)(void *, const char *, ...),
                          void *arg);
#endif /* PRINTPKT_SUPPORT */

/* List of digest types that we know about */
static const struct chap_digest_type *const chap_digests[] = {&md5_digest,
#if MSCHAP_SUPPORT
                                                              &chapms_digest,
                                                              &chapms2_digest,
#endif /* MSCHAP_SUPPORT */
                                                              NULL};

/*
 * chap_init - reset to initial state.
 */
static void chap_init(ppp_pcb *pcb) {
  LWIP_UNUSED_ARG(pcb);

#if 0 /* Not necessary, everything is cleared in ppp_new() */
	memset(&pcb->chap_client, 0, sizeof(chap_client_state));
#if PPP_SERVER
	memset(&pcb->chap_server, 0, sizeof(chap_server_state));
#endif /* PPP_SERVER */
#endif /* 0 */
}

/*
 * chap_lowerup - we can start doing stuff now.
 */
static void chap_lowerup(ppp_pcb *pcb) {

  pcb->chap_client.flags |= LOWERUP;
#if PPP_SERVER
  pcb->chap_server.flags |= LOWERUP;
  if (pcb->chap_server.flags & AUTH_STARTED)
    chap_timeout(pcb);
#endif /* PPP_SERVER */
}

static void chap_lowerdown(ppp_pcb *pcb) {

  pcb->chap_client.flags = 0;
#if PPP_SERVER
  if (pcb->chap_server.flags & TIMEOUT_PENDING)
    UNTIMEOUT(chap_timeout, pcb);
  pcb->chap_server.flags = 0;
#endif /* PPP_SERVER */
}

#if PPP_SERVER
/*
 * chap_auth_peer - Start authenticating the peer.
 * If the lower layer is already up, we start sending challenges,
 * otherwise we wait for the lower layer to come up.
 */
void chap_auth_peer(ppp_pcb *pcb, const char *our_name, int digest_code) {
  const struct chap_digest_type *dp;
  int i;

  if (pcb->chap_server.flags & AUTH_STARTED) {
    ppp_error(("CHAP: peer authentication already started!"));
    return;
  }
  for (i = 0; (dp = chap_digests[i]) != NULL; ++i)
    if (dp->code == digest_code)
      break;
  if (dp == NULL)
    ppp_fatal(("CHAP digest 0x%x requested but not available", digest_code));

  pcb->chap_server.digest = dp;
  pcb->chap_server.name = our_name;
  /* Start with a random ID value */
  pcb->chap_server.id = magic();
  pcb->chap_server.flags |= AUTH_STARTED;
  if (pcb->chap_server.flags & LOWERUP)
    chap_timeout(pcb);
}
#endif /* PPP_SERVER */

/*
 * chap_auth_with_peer - Prepare to authenticate ourselves to the peer.
 * There isn't much to do until we receive a challenge.
 */
void chap_auth_with_peer(ppp_pcb *pcb, const char *our_name, int digest_code) {
  const struct chap_digest_type *dp;
  int i;

  if (NULL == our_name)
    return;

  if (pcb->chap_client.flags & AUTH_STARTED) {
    ppp_error(("CHAP: authentication with peer already started!"));
    return;
  }
  for (i = 0; (dp = chap_digests[i]) != NULL; ++i)
    if (dp->code == digest_code)
      break;

  if (dp == NULL)
    ppp_fatal(("CHAP digest 0x%x requested but not available", digest_code));

  pcb->chap_client.digest = dp;
  pcb->chap_client.name = our_name;
  pcb->chap_client.flags |= AUTH_STARTED;
}

#if PPP_SERVER
/*
 * chap_timeout - It's time to send another challenge to the peer.
 * This could be either a retransmission of a previous challenge,
 * or a new challenge to start re-authentication.
 */
static void chap_timeout(void *arg) {
  ppp_pcb *pcb = (ppp_pcb *)arg;
  struct pbuf *p;

  pcb->chap_server.flags &= ~TIMEOUT_PENDING;
  if ((pcb->chap_server.flags & CHALLENGE_VALID) == 0) {
    pcb->chap_server.challenge_xmits = 0;
    chap_generate_challenge(pcb);
    pcb->chap_server.flags |= CHALLENGE_VALID;
  } else if (pcb->chap_server.challenge_xmits >=
             pcb->settings.chap_max_transmits) {
    pcb->chap_server.flags &= ~CHALLENGE_VALID;
    pcb->chap_server.flags |= AUTH_DONE | AUTH_FAILED;
    auth_peer_fail(pcb, PPP_CHAP);
    return;
  }

  p = pbuf_alloc(PBUF_RAW, (u16_t)(pcb->chap_server.challenge_pktlen),
                 PBUF_RAM);
  if (NULL == p)
    return;
  if (p->tot_len != p->len) {
    pbuf_free(p);
    return;
  }
  MEMCPY(p->payload, pcb->chap_server.challenge,
         pcb->chap_server.challenge_pktlen);
  ppp_write(pcb, p);
  ++pcb->chap_server.challenge_xmits;
  pcb->chap_server.flags |= TIMEOUT_PENDING;
  TIMEOUT(chap_timeout, arg, pcb->settings.chap_timeout_time);
}

/*
 * chap_generate_challenge - generate a challenge string and format
 * the challenge packet in pcb->chap_server.challenge_pkt.
 */
static void chap_generate_challenge(ppp_pcb *pcb) {
  int clen = 1, nlen, len;
  unsigned char *p;

  p = pcb->chap_server.challenge;
  MAKEHEADER(p, PPP_CHAP);
  p += CHAP_HDRLEN;
  pcb->chap_server.digest->generate_challenge(pcb, p);
  clen = *p;
  nlen = strlen(pcb->chap_server.name);
  memcpy(p + 1 + clen, pcb->chap_server.name, nlen);

  len = CHAP_HDRLEN + 1 + clen + nlen;
  pcb->chap_server.challenge_pktlen = PPP_HDRLEN + len;

  p = pcb->chap_server.challenge + PPP_HDRLEN;
  p[0] = CHAP_CHALLENGE;
  p[1] = ++pcb->chap_server.id;
  p[2] = len >> 8;
  p[3] = len;
}

/*
 * chap_handle_response - check the response to our challenge.
 */
static void chap_handle_response(ppp_pcb *pcb, int id, unsigned char *pkt,
                                 int len) {
  int response_len, ok, mlen;
  const unsigned char *response;
  unsigned char *outp;
  struct pbuf *p;
  const char *name = NULL; /* initialized to shut gcc up */
#if 0                      /* UNUSED */
	int (*verifier)(const char *, const char *, int, const struct chap_digest_type *,
		const unsigned char *, const unsigned char *, char *, int);
#endif                     /* UNUSED */
  char rname[MAXNAMELEN + 1];
  char message[256];

  if ((pcb->chap_server.flags & LOWERUP) == 0)
    return;
  if (id != pcb->chap_server.challenge[PPP_HDRLEN + 1] || len < 2)
    return;
  if (pcb->chap_server.flags & CHALLENGE_VALID) {
    response = pkt;
    GETCHAR(response_len, pkt);
    len -= response_len + 1; /* length of name */
    name = (char *)pkt + response_len;
    if (len < 0)
      return;

    if (pcb->chap_server.flags & TIMEOUT_PENDING) {
      pcb->chap_server.flags &= ~TIMEOUT_PENDING;
      UNTIMEOUT(chap_timeout, pcb);
    }
#if PPP_REMOTENAME
    if (pcb->settings.explicit_remote) {
      name = pcb->remote_name;
    } else
#endif /* PPP_REMOTENAME */
    {
      /* Null terminate and clean remote name. */
      ppp_slprintf(rname, sizeof(rname), "%.*v", len, name);
      name = rname;
    }

#if 0  /* UNUSED */
		if (chap_verify_hook)
			verifier = chap_verify_hook;
		else
			verifier = chap_verify_response;
		ok = (*verifier)(name, pcb->chap_server.name, id, pcb->chap_server.digest,
				 pcb->chap_server.challenge + PPP_HDRLEN + CHAP_HDRLEN,
				 response, pcb->chap_server.message, sizeof(pcb->chap_server.message));
#endif /* UNUSED */
    ok = chap_verify_response(
        pcb, name, pcb->chap_server.name, id, pcb->chap_server.digest,
        pcb->chap_server.challenge + PPP_HDRLEN + CHAP_HDRLEN, response,
        message, sizeof(message));
#if 0  /* UNUSED */
		if (!ok || !auth_number()) {
#endif /* UNUSED */
    if (!ok) {
      pcb->chap_server.flags |= AUTH_FAILED;
      ppp_warn(("Peer %q failed CHAP authentication", name));
    }
  } else if ((pcb->chap_server.flags & AUTH_DONE) == 0)
    return;

  /* send the response */
  mlen = strlen(message);
  len = CHAP_HDRLEN + mlen;
  p = pbuf_alloc(PBUF_RAW, (u16_t)(PPP_HDRLEN + len), PBUF_RAM);
  if (NULL == p)
    return;
  if (p->tot_len != p->len) {
    pbuf_free(p);
    return;
  }

  outp = (unsigned char *)p->payload;
  MAKEHEADER(outp, PPP_CHAP);

  outp[0] =
      (pcb->chap_server.flags & AUTH_FAILED) ? CHAP_FAILURE : CHAP_SUCCESS;
  outp[1] = id;
  outp[2] = len >> 8;
  outp[3] = len;
  if (mlen > 0)
    memcpy(outp + CHAP_HDRLEN, message, mlen);
  ppp_write(pcb, p);

  if (pcb->chap_server.flags & CHALLENGE_VALID) {
    pcb->chap_server.flags &= ~CHALLENGE_VALID;
    if (!(pcb->chap_server.flags & AUTH_DONE) &&
        !(pcb->chap_server.flags & AUTH_FAILED)) {

#if 0  /* UNUSED */
		    /*
		     * Auth is OK, so now we need to check session restrictions
		     * to ensure everything is OK, but only if we used a
		     * plugin, and only if we're configured to check.  This
		     * allows us to do PAM checks on PPP servers that
		     * authenticate against ActiveDirectory, and use AD for
		     * account info (like when using Winbind integrated with
		     * PAM).
		     */
		    if (session_mgmt &&
			session_check(name, NULL, devnam, NULL) == 0) {
			pcb->chap_server.flags |= AUTH_FAILED;
			ppp_warn(("Peer %q failed CHAP Session verification", name));
		    }
#endif /* UNUSED */
    }
    if (pcb->chap_server.flags & AUTH_FAILED) {
      auth_peer_fail(pcb, PPP_CHAP);
    } else {
      if ((pcb->chap_server.flags & AUTH_DONE) == 0)
        auth_peer_success(pcb, PPP_CHAP, pcb->chap_server.digest->code, name,
                          strlen(name));
      if (pcb->settings.chap_rechallenge_time) {
        pcb->chap_server.flags |= TIMEOUT_PENDING;
        TIMEOUT(chap_timeout, pcb, pcb->settings.chap_rechallenge_time);
      }
    }
    pcb->chap_server.flags |= AUTH_DONE;
  }
}

/*
 * chap_verify_response - check whether the peer's response matches
 * what we think it should be.  Returns 1 if it does (authentication
 * succeeded), or 0 if it doesn't.
 */
static int chap_verify_response(ppp_pcb *pcb, const char *name,
                                const char *ourname, int id,
                                const struct chap_digest_type *digest,
                                const unsigned char *challenge,
                                const unsigned char *response, char *message,
                                int message_space) {
  int ok;
  unsigned char secret[MAXSECRETLEN];
  int secret_len;

  /* Get the secret that the peer is supposed to know */
  if (!get_secret(pcb, name, ourname, (char *)secret, &secret_len, 1)) {
    ppp_error(("No CHAP secret found for authenticating %q", name));
    return 0;
  }
  ok = digest->verify_response(pcb, id, name, secret, secret_len, challenge,
                               response, message, message_space);
  memset(secret, 0, sizeof(secret));

  return ok;
}
#endif /* PPP_SERVER */

/*
 * chap_respond - Generate and send a response to a challenge.
 */
static void chap_respond(ppp_pcb *pcb, int id, unsigned char *pkt, int len) {
  int clen, nlen;
  int secret_len;
  struct pbuf *p;
  u_char *outp;
  char rname[MAXNAMELEN + 1];
  char secret[MAXSECRETLEN + 1];

  p = pbuf_alloc(PBUF_RAW, (u16_t)(RESP_MAX_PKTLEN), PBUF_RAM);
  if (NULL == p)
    return;
  if (p->tot_len != p->len) {
    pbuf_free(p);
    return;
  }

  if ((pcb->chap_client.flags & (LOWERUP | AUTH_STARTED)) !=
      (LOWERUP | AUTH_STARTED))
    return; /* not ready */
  if (len < 2 || len < pkt[0] + 1)
    return; /* too short */
  clen = pkt[0];
  nlen = len - (clen + 1);

  /* Null terminate and clean remote name. */
  ppp_slprintf(rname, sizeof(rname), "%.*v", nlen, pkt + clen + 1);

#if PPP_REMOTENAME
  /* Microsoft doesn't send their name back in the PPP packet */
  if (pcb->settings.explicit_remote ||
      (pcb->settings.remote_name[0] != 0 && rname[0] == 0))
    strlcpy(rname, pcb->settings.remote_name, sizeof(rname));
#endif /* PPP_REMOTENAME */

  /* get secret for authenticating ourselves with the specified host */
  if (!get_secret(pcb, pcb->chap_client.name, rname, secret, &secret_len, 0)) {
    secret_len = 0; /* assume null secret if can't find one */
    ppp_warn(("No CHAP secret found for authenticating us to %q", rname));
  }

  outp = (u_char *)p->payload;
  MAKEHEADER(outp, PPP_CHAP);
  outp += CHAP_HDRLEN;

  pcb->chap_client.digest->make_response(pcb, outp, id, pcb->chap_client.name,
                                         pkt, secret, secret_len,
                                         pcb->chap_client.priv);
  memset(secret, 0, secret_len);

  clen = *outp;
  nlen = strlen(pcb->chap_client.name);
  memcpy(outp + clen + 1, pcb->chap_client.name, nlen);

  outp = (u_char *)p->payload + PPP_HDRLEN;
  len = CHAP_HDRLEN + clen + 1 + nlen;
  outp[0] = CHAP_RESPONSE;
  outp[1] = id;
  outp[2] = len >> 8;
  outp[3] = len;

  pbuf_realloc(p, PPP_HDRLEN + len);
  ppp_write(pcb, p);
}

static void chap_handle_status(ppp_pcb *pcb, int code, int id,
                               unsigned char *pkt, int len) {
  const char *msg = NULL;
  LWIP_UNUSED_ARG(id);

  if ((pcb->chap_client.flags & (AUTH_DONE | AUTH_STARTED | LOWERUP)) !=
      (AUTH_STARTED | LOWERUP))
    return;
  pcb->chap_client.flags |= AUTH_DONE;

  if (code == CHAP_SUCCESS) {
    /* used for MS-CHAP v2 mutual auth, yuck */
    if (pcb->chap_client.digest->check_success != NULL) {
      if (!(*pcb->chap_client.digest->check_success)(pcb, pkt, len,
                                                     pcb->chap_client.priv))
        code = CHAP_FAILURE;
    } else
      msg = "CHAP authentication succeeded";
  } else {
    if (pcb->chap_client.digest->handle_failure != NULL)
      (*pcb->chap_client.digest->handle_failure)(pcb, pkt, len);
    else
      msg = "CHAP authentication failed";
  }
  if (msg) {
    if (len > 0)
      ppp_info(("%s: %.*v", msg, len, pkt));
    else
      ppp_info(("%s", msg));
  }
  if (code == CHAP_SUCCESS)
    auth_withpeer_success(pcb, PPP_CHAP, pcb->chap_client.digest->code);
  else {
    pcb->chap_client.flags |= AUTH_FAILED;
    ppp_error(("CHAP authentication failed"));
    auth_withpeer_fail(pcb, PPP_CHAP);
  }
}

static void chap_input(ppp_pcb *pcb, unsigned char *pkt, int pktlen) {
  unsigned char code, id;
  int len;

  if (pktlen < CHAP_HDRLEN)
    return;
  GETCHAR(code, pkt);
  GETCHAR(id, pkt);
  GETSHORT(len, pkt);
  if (len < CHAP_HDRLEN || len > pktlen)
    return;
  len -= CHAP_HDRLEN;

  switch (code) {
  case CHAP_CHALLENGE:
    chap_respond(pcb, id, pkt, len);
    break;
#if PPP_SERVER
  case CHAP_RESPONSE:
    chap_handle_response(pcb, id, pkt, len);
    break;
#endif /* PPP_SERVER */
  case CHAP_FAILURE:
  case CHAP_SUCCESS:
    chap_handle_status(pcb, code, id, pkt, len);
    break;
  default:
    break;
  }
}

static void chap_protrej(ppp_pcb *pcb) {

#if PPP_SERVER
  if (pcb->chap_server.flags & TIMEOUT_PENDING) {
    pcb->chap_server.flags &= ~TIMEOUT_PENDING;
    UNTIMEOUT(chap_timeout, pcb);
  }
  if (pcb->chap_server.flags & AUTH_STARTED) {
    pcb->chap_server.flags = 0;
    auth_peer_fail(pcb, PPP_CHAP);
  }
#endif /* PPP_SERVER */
  if ((pcb->chap_client.flags & (AUTH_STARTED | AUTH_DONE)) == AUTH_STARTED) {
    pcb->chap_client.flags &= ~AUTH_STARTED;
    ppp_error(("CHAP authentication failed due to protocol-reject"));
    auth_withpeer_fail(pcb, PPP_CHAP);
  }
}

#if PRINTPKT_SUPPORT
/*
 * chap_print_pkt - print the contents of a CHAP packet.
 */
static const char *const chap_code_names[] = {"Challenge", "Response",
                                              "Success", "Failure"};

static int chap_print_pkt(const unsigned char *p, int plen,
                          void (*printer)(void *, const char *, ...),
                          void *arg) {
  int code, id, len;
  int clen, nlen;
  unsigned char x;

  if (plen < CHAP_HDRLEN)
    return 0;
  GETCHAR(code, p);
  GETCHAR(id, p);
  GETSHORT(len, p);
  if (len < CHAP_HDRLEN || len > plen)
    return 0;

  if (code >= 1 && code <= (int)LWIP_ARRAYSIZE(chap_code_names))
    printer(arg, " %s", chap_code_names[code - 1]);
  else
    printer(arg, " code=0x%x", code);
  printer(arg, " id=0x%x", id);
  len -= CHAP_HDRLEN;
  switch (code) {
  case CHAP_CHALLENGE:
  case CHAP_RESPONSE:
    if (len < 1)
      break;
    clen = p[0];
    if (len < clen + 1)
      break;
    ++p;
    nlen = len - clen - 1;
    printer(arg, " <");
    for (; clen > 0; --clen) {
      GETCHAR(x, p);
      printer(arg, "%.2x", x);
    }
    printer(arg, ">, name = ");
    ppp_print_string(p, nlen, printer, arg);
    break;
  case CHAP_FAILURE:
  case CHAP_SUCCESS:
    printer(arg, " ");
    ppp_print_string(p, len, printer, arg);
    break;
  default:
    for (clen = len; clen > 0; --clen) {
      GETCHAR(x, p);
      printer(arg, " %.2x", x);
    }
    /* no break */
  }

  return len + CHAP_HDRLEN;
}
#endif /* PRINTPKT_SUPPORT */

const struct protent chap_protent = {
  PPP_CHAP,
  chap_init,
  chap_input,
  chap_protrej,
  chap_lowerup,
  chap_lowerdown,
  NULL, /* open */
  NULL, /* close */
#if PRINTPKT_SUPPORT
  chap_print_pkt,
#endif /* PRINTPKT_SUPPORT */
#if PPP_DATAINPUT
  NULL, /* datainput */
#endif  /* PPP_DATAINPUT */
#if PRINTPKT_SUPPORT
  "CHAP", /* name */
  NULL,   /* data_name */
#endif    /* PRINTPKT_SUPPORT */
#if PPP_OPTIONS
  chap_option_list,
  NULL, /* check_options */
#endif  /* PPP_OPTIONS */
#if DEMAND_SUPPORT
  NULL,
  NULL
#endif /* DEMAND_SUPPORT */
};

#endif /* PPP_SUPPORT && CHAP_SUPPORT */
